package com.ld.hpfs.common.utils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

import com.ld.hpfs.common.utils.DoubleUtil;
import com.ld.hpfs.common.utils.IntegerUtil;

public class SplitTools {
    public SplitTools(Double maxLimitAmount, boolean splitByPrice, Integer ignoreOverFlow) {
        maxLimit = maxLimitAmount;//最大限额
        splitByQuantity = !splitByPrice;
        ignoreNum = ignoreOverFlow;

        numberFormat.setGroupingUsed(false);
        numberFormat.setMaximumFractionDigits(exactitude);
        numberFormat.setRoundingMode(roundingMode);
    }
    private static final int excludingTax = 0;
    private static final int includingTax = 1;
//得到拆分的数目组合
    public List<Double> getSplitNumbers(String idHeadCode,List<Double> nums, Double grain, Double step, Double price, Double quantity, Double taxRate, Double totalAmount, Double totalTaxes, Double discountTaxRate, Double discountAmount, Integer priceWays, boolean withTrialCalculation) throws Exception {
    	splitDateTime = Calendar.getInstance();
    	Date dateStart = new Date();  
    	splitDateTime.setTime(dateStart);  
    	BigDecimal bigGrain = new BigDecimal(String.valueOf(grain));//拆分试算精度（小数）
        BigDecimal bigStep = new BigDecimal(String.valueOf(step));//拆分试算步长（小数）
        BigDecimal bigPrice = new BigDecimal(String.valueOf(price));//价格
        BigDecimal bigQuantity = new BigDecimal(String.valueOf(quantity));//数量
        BigDecimal bigTaxRate = new BigDecimal(String.valueOf(taxRate));//税率
        BigDecimal bigTotalAmount = new BigDecimal(String.valueOf(totalAmount));//本币金额
        BigDecimal bigTotalTaxes = new BigDecimal(String.valueOf(totalTaxes));//税额
        BigDecimal bigDiscountTaxRate = new BigDecimal(String.valueOf(discountTaxRate));//折扣率
        BigDecimal bigDiscountAmount = new BigDecimal(String.valueOf(discountAmount));//折扣金额

        amountMiss = 0.0;
        taxesMiss = 0.0;
        //折扣数量差额
        discountAmountMiss = 0.0;
        discountTaxesMiss = 0.0;
        priceWay = priceWays;

        setValidMaxAmount(bigTaxRate);
        ifFirst = true;

        if (totalAmount > maxAmount.doubleValue()) {
            if (splitByQuantity) {
                if (price > maxAmount.doubleValue()) {
                    bigGrain = minGrain;
                    bigStep = minStep;
                }
                //试验估算
                if (withTrialCalculation) {
                	double totalTaxesMiss = Math.abs(getValidTaxes(bigTotalAmount, bigTaxRate).subtract(bigTotalTaxes).doubleValue());
                	if (DoubleUtil.isSame(totalTaxesMiss, 0.0)) {
                		splitByTrialCalculation(idHeadCode,nums, bigGrain, bigStep, bigQuantity, bigPrice, bigTaxRate, bigTotalAmount, bigDiscountTaxRate, bigDiscountAmount, getMaxSizeOfSplited(totalAmount));
                	} else if (totalTaxesMiss <= ALLOW_TAXES_MISS) {
                		splitByTrialCalculation(nums, bigGrain, bigStep, bigQuantity, bigPrice, bigTaxRate, bigTotalAmount, bigTotalTaxes, bigDiscountTaxRate, bigDiscountAmount, getMaxSizeOfSplited(totalAmount));
                		detailTaxesMissSum = getSplitedTaxMiss(nums, bigTotalTaxes, bigTaxRate, bigPrice);
                	} else {
                		throw new RuntimeException("Taxes does not meet tax regulation and the difference between correct and current is more than 0.01.");
                	}
                } else {
                    splitWithOutTrialCalculation(nums, bigGrain, bigQuantity, bigPrice, bigTaxRate, bigTotalAmount, bigDiscountTaxRate, bigDiscountAmount);
                    taxesMiss = DoubleUtil.sub(DoubleUtil.ZERO, getSplitedTaxMiss(nums, bigTotalTaxes, bigTaxRate, bigPrice));
                }
            } else {
                if (withTrialCalculation) {
                	double totalTaxesMiss = Math.abs(getValidTaxes(bigTotalAmount, bigTaxRate).subtract(bigTotalTaxes).doubleValue());
                	if (DoubleUtil.isSame(totalTaxesMiss, 0.0)) {
                		splitByTrialCalculation(idHeadCode,nums, bigGrain, bigStep, bigPrice, bigQuantity, bigTaxRate, bigTotalAmount, bigDiscountTaxRate, bigDiscountAmount, getMaxSizeOfSplited(totalAmount));
                	} else if (totalTaxesMiss <= ALLOW_TAXES_MISS) {
                		splitByTrialCalculation(nums, bigGrain, bigStep, bigPrice, bigQuantity, bigTaxRate, bigTotalAmount, bigTotalTaxes, bigDiscountTaxRate, bigDiscountAmount, getMaxSizeOfSplited(totalAmount));
                		detailTaxesMissSum = getSplitedTaxMiss(nums, bigTotalTaxes, bigTaxRate, bigQuantity);
                	} else {
                		throw new RuntimeException("Taxes by calculate, is miss over 0.01 with give taxes.");
                	}
                } else {
                    splitWithOutTrialCalculation(nums, bigGrain, bigPrice, bigQuantity, bigTaxRate, bigTotalAmount, bigDiscountTaxRate, bigDiscountAmount);
                    taxesMiss = DoubleUtil.sub(DoubleUtil.ZERO, getSplitedTaxMiss(nums, bigTotalTaxes, bigTaxRate, bigQuantity));
                }
            }
        } else {
            return nums;
        }
        isContinue = false;
        return nums;
    }
    //得到拆分税额差
    private double getSplitedTaxMiss(List<Double> nums, BigDecimal origTotalTaxes, BigDecimal taxRate, BigDecimal factor) {
    	BigDecimal totalTaxes = new BigDecimal(String.valueOf(DoubleUtil.ZERO));
    	for (Double num : nums) {
    		totalTaxes = totalTaxes.add(getValidTaxes(formatedDecimal(factor.multiply(new BigDecimal(String.valueOf(num)))), taxRate));
    	}
    	return totalTaxes.subtract(origTotalTaxes).doubleValue();
    }
    
    /**
     * @param splitedNums :This list design to store splited out numbers. when return size equal zaro, explain split had not successfully.
     * @param grain :a size, return number must greater then grain and return number mod grain must equal zaro.
     * @param adjustmentStep :When trial calculate, if a number not a valid return number, use adjustmentStep to adjust the number for next trial.
     * @param needSplitNum :a number which need split to several numbers.
     * @param factor :
     * @param taxRate :
     * @param totalAmount :Normally, neesSplitNum multiply factor equal totalAmount.
     * @param maxInvNum :max num by splited.
     * This algorithm for return a list which has several numbers. these number has following relations:
     *         1.Sum of these numbers is must equal needSplitNum.
     *         2.If Amount signify every number multiply factor, then the sum of Amount is equal totalAmount,
     *   and every Amount multiply taxRate equal totalAmount multiply taxRate.
     * @throws Exception 
     */
    private List<Double> splitByTrialCalculation(String idHeadCode,List<Double> splitedNums, BigDecimal grain, BigDecimal adjustmentStep,
                                                 BigDecimal needSplitNum, BigDecimal factor, BigDecimal taxRate, BigDecimal totalAmount,
                                                 BigDecimal discountTaxRate, BigDecimal discountAmount, int maxInvNum) throws Exception {
    	//比较拆分时间
    	Calendar splitTime = Calendar.getInstance();
    	Date dateStop = new Date();  
    	splitTime.setTime(dateStop);  
    	int splitMinute = splitTime.get(Calendar.MINUTE)-splitDateTime.get(Calendar.MINUTE);
    	if(splitMinute>10){
    		splitedNums.clear();
    		throw new RuntimeException("单据"+idHeadCode+"拆分的结果将会有尾差,是否允许有尾差拆分");
    	}
        maxInvNum = maxInvNum - 1;
        if (maxInvNum < 0) {
            isContinue = true;
            return splitedNums;
        }

        if (totalAmount.compareTo(maxAmount) < 0) {
            if (needSplitNum.doubleValue() != 0.0) {
                splitedNums.add(needSplitNum.doubleValue());
            }
            isContinue = false;
            return splitedNums;
        }

        BigDecimal totalTaxes = getValidTaxes(totalAmount, taxRate);
        BigDecimal totalDiscountTaxes = getValidTaxes(discountAmount, taxRate);
        BigDecimal maxNum = maxAmount.divide(factor, exactitude, roundingMode);
        BigDecimal modMaxNum = needSplitNum.subtract(maxNum);

        //if maxNum multiply factor greater then maxAmount or maxNum is not a valid return num, then adjust maxNum and modMaxNum, for ensure maxNum is a valid maxNum.
        if (maxNum.multiply(factor).compareTo(maxAmount) > 0 || !isValidReturnNum(maxNum, grain)) {
            do {
                maxNum = maxNum.subtract(minStep);
                modMaxNum = modMaxNum.add(minStep);
            } while (!isValidReturnNum(maxNum, grain));
        }

        if (DoubleUtil.isSame(maxNum.doubleValue(), DoubleUtil.ZERO) && ifFirst) {
    		ifFirst = false;
    		throw new Exception("max number of splited is zero.");
        }
        
        for (BigDecimal maxValidNum = maxNum; maxValidNum.compareTo(grain)>0; maxValidNum = maxValidNum.subtract(adjustmentStep)) {
            BigDecimal subTotalAmount = formatedDecimal(factor.multiply(maxValidNum));
            BigDecimal modSubTotalAmount = formatedDecimal(factor.multiply(modMaxNum));
            BigDecimal sumTaxes = getValidTaxes(subTotalAmount, taxRate).add(getValidTaxes(modSubTotalAmount, taxRate));
            BigDecimal subDiscountAmount = getValidDiscountAmount(subTotalAmount, discountTaxRate);
            BigDecimal modSubDiscountAmount = getValidDiscountAmount(modSubTotalAmount, discountTaxRate);
            BigDecimal sumDiscountTaxes = getValidTaxes(subDiscountAmount, taxRate).add(getValidTaxes(modSubDiscountAmount, taxRate));
            if (((maxValidNum.add(modMaxNum)).compareTo(needSplitNum) == 0)
                    && (totalAmount.compareTo(subTotalAmount.add(modSubTotalAmount)) == 0)
                    && (sumTaxes.compareTo(totalTaxes) == 0)
                    && (discountAmount.compareTo(subDiscountAmount.add(modSubDiscountAmount)) == 0)
                    && (sumDiscountTaxes.compareTo(totalDiscountTaxes) == 0)) {
                
                if (isValidReturnNum(maxValidNum, grain)) {
                    splitByTrialCalculation(idHeadCode,splitedNums, grain, adjustmentStep, modMaxNum, factor, taxRate, modSubTotalAmount, discountTaxRate, modSubDiscountAmount, maxInvNum);
                    if (!isContinue) {
                        splitedNums.add(maxValidNum.doubleValue());
                        isContinue = false;
                        return splitedNums;
                    }
                }
            }

            modMaxNum = modMaxNum.add(adjustmentStep);//corresponding with (maxValidNum = maxValidNum.subtract(adjustmentStep)).
        }
        isContinue = true;
        return splitedNums;
    }
    
    /**
     * @param splitedNums :This list design to store splited out numbers. when return size equal zaro, explain split had not successfully.
     * @param grain :a size, return number must greater then grain and return number mod grain must equal zaro.
     * @param adjustmentStep :When trial calculate, if a number not a valid return number, use adjustmentStep to adjust the number for next trial.
     * @param needSplitNum :a number which need split to several numbers.
     * @param factor :
     * @param taxRate :
     * @param totalAmount :Normally, neesSplitNum multiply factor equal totalAmount.
     * @param totalTaxes :Normally, sum (neesSplitNum multiply factor multiply taxRate) equal totalTaxes.
     * @param maxInvNum :max num by splited.
     * This algorithm for return a list which has several numbers. these number has following relations:
     *         1.Sum of these numbers is must equal needSplitNum.
     *         2.If Amount signify every number multiply factor, then the sum of Amount is equal totalAmount,
     *   and every Amount multiply taxRate equal totalAmount multiply taxRate.
     * @throws Exception 
     */
    private List<Double> splitByTrialCalculation(List<Double> splitedNums, BigDecimal grain, BigDecimal adjustmentStep,
                                                 BigDecimal needSplitNum, BigDecimal factor, BigDecimal taxRate, BigDecimal totalAmount, BigDecimal totalTaxes,
                                                 BigDecimal discountTaxRate, BigDecimal discountAmount, int maxInvNum) throws Exception {
    	maxInvNum = maxInvNum - 1;
        if (maxInvNum < 0) {
            isContinue = true;
            return splitedNums;
        }

        if (totalAmount.compareTo(maxAmount) < 0) {
            if (needSplitNum.doubleValue() != 0.0) {
                splitedNums.add(needSplitNum.doubleValue());
            }
            isContinue = false;
            return splitedNums;
        }

        BigDecimal totalDiscountTaxes = getValidTaxes(discountAmount, taxRate);
        BigDecimal maxNum = maxAmount.divide(factor, exactitude, roundingMode);
        BigDecimal modMaxNum = needSplitNum.subtract(maxNum);

        //if maxNum multiply factor greater then maxAmount or maxNum is not a valid return num, then adjust maxNum and modMaxNum, for ensure maxNum is a valid maxNum.
        if (maxNum.multiply(factor).compareTo(maxAmount) > 0 || !isValidReturnNum(maxNum, grain)) {
            do {
                maxNum = maxNum.subtract(minStep);
                modMaxNum = modMaxNum.add(minStep);
            } while (!isValidReturnNum(maxNum, grain));
        }

        if (DoubleUtil.isSame(maxNum.doubleValue(), DoubleUtil.ZERO) && ifFirst) {
    		ifFirst = false;
    		throw new Exception("max number of splited is zero.");
        }
        
        for (BigDecimal maxValidNum = maxNum; maxValidNum.compareTo(grain)>0; maxValidNum = maxValidNum.subtract(adjustmentStep)) {
        	BigDecimal subTotalAmount = formatedDecimal(factor.multiply(maxValidNum));
            BigDecimal modSubTotalAmount = formatedDecimal(factor.multiply(modMaxNum));
            BigDecimal subTaxes = getValidTaxes(subTotalAmount, taxRate);
            BigDecimal modTaxes = getValidTaxes(modSubTotalAmount, taxRate);
            BigDecimal remainTaxes = totalTaxes.subtract(subTaxes);
            
            BigDecimal subDiscountAmount = getValidDiscountAmount(subTotalAmount, discountTaxRate);
            BigDecimal modSubDiscountAmount = getValidDiscountAmount(modSubTotalAmount, discountTaxRate);
            BigDecimal sumDiscountTaxes = getValidTaxes(subDiscountAmount, taxRate).add(getValidTaxes(modSubDiscountAmount, taxRate));
            double taxMiss = Math.abs(remainTaxes.subtract(modTaxes).doubleValue());
            if (((maxValidNum.add(modMaxNum)).compareTo(needSplitNum) == 0)
                    && (totalAmount.compareTo(subTotalAmount.add(modSubTotalAmount)) == 0)
                    && (DoubleUtil.add(taxMiss, detailTaxesMiss) <= ALLOW_TAXES_MISS)
                    && (discountAmount.compareTo(subDiscountAmount.add(modSubDiscountAmount)) == 0)
                    && (sumDiscountTaxes.compareTo(totalDiscountTaxes) == 0)) {
                if (isValidReturnNum(maxValidNum, grain)) {
                	boolean isHasMiss = false;
                	if (!DoubleUtil.isSame(taxMiss, DoubleUtil.ZERO)) {
                		isHasMiss = true;
                		detailTaxesMiss = DoubleUtil.add(detailTaxesMiss, taxMiss);
                	}
                	
                    splitByTrialCalculation(splitedNums, grain, adjustmentStep, modMaxNum, factor, taxRate, modSubTotalAmount, remainTaxes, discountTaxRate, modSubDiscountAmount, maxInvNum);
                    if (!isContinue) {
                        splitedNums.add(maxValidNum.doubleValue());
                        isContinue = false;
                        return splitedNums;
                    } else {
                    	if (isHasMiss) {
                    		detailTaxesMiss = DoubleUtil.sub(detailTaxesMiss, taxMiss);
                    	}
                    }
                }
            }

            modMaxNum = modMaxNum.add(adjustmentStep);//corresponding with (maxValidNum = maxValidNum.subtract(adjustmentStep)).
        }
        isContinue = true;
        return splitedNums;
    }

    private List<Double> splitWithOutTrialCalculation(List<Double> splitedNums, BigDecimal grain,
                                                 BigDecimal needSplitNum, BigDecimal factor, BigDecimal taxRate, BigDecimal totalAmount,
                                                 BigDecimal discountTaxRate, BigDecimal discountAmount) throws Exception {
    	//grain拆分试算精度（小数）
        //factor价格
        //needSplitNum 数量
        //taxRate税率
        //totalAmount本币金额
        //discountTaxRate折扣率
        //discountAmount折扣金额
        if (totalAmount.compareTo(maxAmount) < 0) {
            if (needSplitNum.doubleValue() != 0.0) {
                splitedNums.add(needSplitNum.doubleValue());
            }
            return splitedNums;
        }

        BigDecimal maxNum = maxAmount.divide(factor, exactitude, roundingMode);
        BigDecimal modMaxNum = needSplitNum.subtract(maxNum);
        
        //if maxNum multiply factor greater then maxAmount or maxNum is not a valid return num, then adjust maxNum and modMaxNum, for ensure maxNum is a valid maxNum.
        if (maxNum.multiply(factor).compareTo(maxAmount) > 0 || !isValidReturnNum(maxNum, grain)) {
            if (maxNum.compareTo(grain) <= 0) {
                grain = minGrain;
            }
            
            do {
                maxNum = maxNum.subtract(minStep);
                modMaxNum = modMaxNum.add(minStep);
            } while (!isValidReturnNum(maxNum, grain));
        }

        if (DoubleUtil.isSame(maxNum.doubleValue(), DoubleUtil.ZERO) && ifFirst) {
    		ifFirst = false;
    		throw new Exception("max number of splited is zero.");
        }
        BigDecimal modSubTotalAmount = formatedDecimal(factor.multiply(modMaxNum));
        BigDecimal modSubDiscountAmount = getValidDiscountAmount(modSubTotalAmount, discountTaxRate);
        splitWithOutTrialCalculation(splitedNums, grain, modMaxNum, factor, taxRate, modSubTotalAmount, discountTaxRate, modSubDiscountAmount);

        splitedNums.add(maxNum.doubleValue());

        calculateOverAndShort(splitedNums, factor, taxRate, totalAmount, discountTaxRate, discountAmount);
        return splitedNums;
    }

    private void calculateOverAndShort(List<Double> splitedNums, BigDecimal factor, BigDecimal taxRate, BigDecimal totalAmount, BigDecimal discountTaxRate, BigDecimal discountAmount) {
        BigDecimal tempAmount;
        BigDecimal tempAmounts = new BigDecimal(String.valueOf(0.0));
        BigDecimal tempTaxes = new BigDecimal(String.valueOf(0.0));
        BigDecimal tempDiscountAmounts = new BigDecimal(String.valueOf(0.0));
        BigDecimal tempDiscountTaxes = new BigDecimal(String.valueOf(0.0));

        for (Double oneNum : splitedNums) {
            tempAmount = formatedDecimal(factor.multiply(new BigDecimal(String.valueOf(oneNum))));
            tempAmounts = tempAmounts.add(tempAmount);
            tempTaxes = tempTaxes.add(getValidTaxes(tempAmount, taxRate));
            tempDiscountAmounts = tempDiscountAmounts.add(getValidDiscountAmount(tempAmount, discountTaxRate));
            tempDiscountTaxes = tempDiscountTaxes.add(getValidTaxes(getValidDiscountAmount(tempAmount, discountTaxRate), taxRate));
        }

        amountMiss = totalAmount.subtract(tempAmounts).doubleValue();
        taxesMiss = getValidTaxes(totalAmount, taxRate).subtract(tempTaxes).doubleValue();
        discountAmountMiss = discountAmount.subtract(tempDiscountAmounts).doubleValue();
        discountTaxesMiss = getValidTaxes(discountAmount, taxRate).subtract(tempDiscountTaxes).doubleValue();
    }

    private boolean isValidReturnNum(BigDecimal returnNum, BigDecimal grain) {
        return (returnNum.doubleValue()*100)%(grain.doubleValue()*100) == 0;
    }

    private BigDecimal formatedDecimal (BigDecimal dec) {
        return  new BigDecimal(numberFormat.format(dec));
    }
//得到有效税率
    private BigDecimal getValidTaxes(BigDecimal amount, BigDecimal taxRate) {
        switch (priceWay) {
            case excludingTax:
                return formatedDecimal(amount.multiply(taxRate));
            case includingTax:
                return formatedDecimal(amount.divide(taxRate.add(new BigDecimal(String.valueOf(1.0))), sixExactitude, roundingMode).multiply(taxRate));
            default:
                return formatedDecimal(amount.multiply(taxRate));
        }
    }
//得到有效折扣数目
    private BigDecimal getValidDiscountAmount(BigDecimal discountSum, BigDecimal discountTaxRate) {
        return formatedDecimal(discountSum.multiply(discountTaxRate).divide(new BigDecimal(String.valueOf(100.0))));
    }
//设置有效最大数目
    private void setValidMaxAmount(BigDecimal taxRate) {
        maxAmount = new BigDecimal(String.valueOf(maxLimit));
        switch (priceWay) {
            case excludingTax:
                return;
            case includingTax:
                maxAmount = maxAmount.multiply(taxRate.add(new BigDecimal(String.valueOf(1.0))));
                return;
            default:
                return;
        }
    }
//得到最大拆分尺度
    private int getMaxSizeOfSplited(Double totalAmount) {
    	if (IntegerUtil.isSame(ignoreNum, 0)) {
    		return Double.valueOf(DoubleUtil.mul(DoubleUtil.add(DoubleUtil.div(totalAmount, maxAmount.doubleValue()), 1.0), 2.0)).intValue();
    	} else {
    		return ignoreNum + (Double.valueOf(DoubleUtil.add(DoubleUtil.div(totalAmount, maxAmount.doubleValue()), 1.0)).intValue());
    	}
    }
    
    public double detailTaxesMissSum = DoubleUtil.ZERO;
    
    private double ALLOW_TAXES_MISS = 0.01;
    private Double detailTaxesMiss = DoubleUtil.ZERO;

    private int ignoreNum = 0;

    private boolean ifFirst;

    private static NumberFormat numberFormat = NumberFormat.getInstance();
    private static boolean isContinue = false;//isContinue control if isContinue is false, meaning recursion return a valid result.
    private static boolean splitByQuantity;
    private static BigDecimal maxAmount;
    private static Double maxLimit;

    private final int exactitude = 2;
    private final int sixExactitude = 6;
    private final BigDecimal minGrain = new BigDecimal(String.valueOf(0.01));
    private final BigDecimal minStep = new BigDecimal(String.valueOf(0.01));

    private final RoundingMode roundingMode = RoundingMode.HALF_UP;

    public static Double amountMiss = 0.0;
    public static Double taxesMiss = 0.0;
    public static Double discountAmountMiss = 0.0;
    public static Double discountTaxesMiss = 0.0;
    public static int priceWay = excludingTax;
    public static Calendar splitDateTime = null;
}